/**
 *
 * Copyright 2003-2006 Jive Software.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.filetransfer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.jxmpp.jid.Jid;

/**
 * Contains the generic file information and progress related to a particular
 * file transfer.
 *
 * @author Alexander Wenckus
 *
 */
public abstract class FileTransfer {

    private String fileName;

    private String filePath;

    private long fileSize;

    private Jid peer;

    private Status status = Status.initial;

    private final Object statusMonitor = new Object();

    protected FileTransferNegotiator negotiator;

    protected String streamID;

    protected long amountWritten = -1;

    private Error error;

    private Exception exception;

    /**
     * Buffer size between input and output
     */
    private static final int BUFFER_SIZE = 8192;

    protected FileTransfer(Jid peer, String streamID,
            FileTransferNegotiator negotiator) {
        this.peer = peer;
        this.streamID = streamID;
        this.negotiator = negotiator;
    }

    protected void setFileInfo(String fileName, long fileSize) {
        this.fileName = fileName;
        this.fileSize = fileSize;
    }

    protected void setFileInfo(String path, String fileName, long fileSize) {
        this.filePath = path;
        this.fileName = fileName;
        this.fileSize = fileSize;
    }

    /**
     * Returns the size of the file being transferred.
     *
     * @return Returns the size of the file being transferred.
     */
    public long getFileSize() {
        return fileSize;
    }

    /**
     * Returns the name of the file being transferred.
     *
     * @return Returns the name of the file being transferred.
     */
    public String getFileName() {
        return fileName;
    }

    /**
     * Returns the local path of the file.
     *
     * @return Returns the local path of the file.
     */
    public String getFilePath() {
        return filePath;
    }

    /**
     * Returns the JID of the peer for this file transfer.
     *
     * @return Returns the JID of the peer for this file transfer.
     */
    public Jid getPeer() {
        return peer;
    }

    /**
     * Returns the progress of the file transfer as a number between 0 and 1.
     *
     * @return Returns the progress of the file transfer as a number between 0
     *         and 1.
     */
    public double getProgress() {
        if (amountWritten <= 0 || fileSize <= 0) {
            return 0;
        }
        return (double) amountWritten / (double) fileSize;
    }

    /**
     * Returns true if the transfer has been cancelled, if it has stopped because
     * of a an error, or the transfer completed successfully.
     *
     * @return Returns true if the transfer has been cancelled, if it has stopped
     *         because of a an error, or the transfer completed successfully.
     */
    public boolean isDone() {
        return status == Status.cancelled || status == Status.error
                || status == Status.complete || status == Status.refused;
    }

    /**
     * Returns the current status of the file transfer.
     *
     * @return Returns the current status of the file transfer.
     */
    public Status getStatus() {
        return status;
    }

    protected void setError(Error type) {
        this.error = type;
    }

    /**
     * When {@link #getStatus()} returns that there was an {@link Status#error}
     * during the transfer, the type of error can be retrieved through this
     * method.
     *
     * @return Returns the type of error that occurred if one has occurred.
     */
    public Error getError() {
        return error;
    }

    /**
     * If an exception occurs asynchronously it will be stored for later
     * retrieval. If there is an error there maybe an exception set.
     *
     * @return The exception that occurred or null if there was no exception.
     * @see #getError()
     */
    public Exception getException() {
        return exception;
    }

    public String getStreamID() {
        return streamID;
    }

    /**
     * Cancels the file transfer.
     */
    public abstract void cancel();

    protected void setException(Exception exception) {
        this.exception = exception;
        Status currentStatus = getStatus();
        if (currentStatus != Status.error) {
            updateStatus(currentStatus, Status.error);
        }
    }

    protected void setStatus(Status status) {
        synchronized (statusMonitor) {
         // CHECKSTYLE:OFF
            this.status = status;
        }
        // CHECKSTYLE:ON
    }

    protected boolean updateStatus(Status oldStatus, Status newStatus) {
        synchronized (statusMonitor) {
            if (oldStatus != status) {
                return false;
            }
            status = newStatus;
            return true;
        }
    }

    protected void writeToStream(final InputStream in, final OutputStream out)
                    throws IOException {
        final byte[] b = new byte[BUFFER_SIZE];
        int count = 0;
        amountWritten = 0;

        while ((count = in.read(b)) > 0 && !getStatus().equals(Status.cancelled)) {
            out.write(b, 0, count);
            amountWritten += count;
        }

        // When the amount of data written does not equal the expected amount, and
        // the transfer was not explicitly cancelled, register an error (unless another
        // error has already been logged).
        if (!getStatus().equals(Status.cancelled) && getError() == null
                && amountWritten != fileSize) {
            setStatus(Status.error);
            this.error = Error.connection;
        }
    }

    /**
     * A class to represent the current status of the file transfer.
     *
     * @author Alexander Wenckus
     *
     */
    public enum Status {

        /**
         * An error occurred during the transfer.
         *
         * @see FileTransfer#getError()
         */
        error("Error"),

        /**
         * The initial status of the file transfer.
         */
        initial("Initial"),

        /**
         * The file transfer is being negotiated with the peer. The party
         * Receiving the file has the option to accept or refuse a file transfer
         * request. If they accept, then the process of stream negotiation will
         * begin. If they refuse the file will not be transferred.
         *
         * @see #negotiating_stream
         */
        negotiating_transfer("Negotiating Transfer"),

        /**
         * The peer has refused the file transfer request halting the file
         * transfer negotiation process.
         */
        refused("Refused"),

        /**
         * The stream to transfer the file is being negotiated over the chosen
         * stream type. After the stream negotiating process is complete the
         * status becomes negotiated.
         *
         * @see #negotiated
         */
        negotiating_stream("Negotiating Stream"),

        /**
         * After the stream negotiation has completed the intermediate state
         * between the time when the negotiation is finished and the actual
         * transfer begins.
         */
        negotiated("Negotiated"),

        /**
         * The transfer is in progress.
         *
         * @see FileTransfer#getProgress()
         */
        in_progress("In Progress"),

        /**
         * The transfer has completed successfully.
         */
        complete("Complete"),

        /**
         * The file transfer was cancelled.
         */
        cancelled("Cancelled");

        private final String status;

        Status(String status) {
            this.status = status;
        }

        @Override
        public String toString() {
            return status;
        }
    }

    /**
     * Return the length of bytes written out to the stream.
     * @return the amount in bytes written out.
     */
    public long getAmountWritten() {
        return amountWritten;
    }

    @SuppressWarnings("JavaLangClash")
    public enum Error {
        /**
         * The peer did not find any of the provided stream mechanisms
         * acceptable.
         */
        not_acceptable("The peer did not find any of the provided stream mechanisms acceptable."),

        /**
         * The provided file to transfer does not exist or could not be read.
         */
        bad_file("The provided file to transfer does not exist or could not be read."),

        /**
         * The remote user did not respond or the connection timed out.
         */
        no_response("The remote user did not respond or the connection timed out."),

        /**
         * An error occurred over the socket connected to send the file.
         */
        connection("An error occurred over the socket connected to send the file."),

        /**
         * An error occurred while sending or receiving the file.
         */
        stream("An error occurred while sending or receiving the file.");

        private final String msg;

        Error(String msg) {
            this.msg = msg;
        }

        /**
         * Returns a String representation of this error.
         *
         * @return Returns a String representation of this error.
         */
        public String getMessage() {
            return msg;
        }

        @Override
        public String toString() {
            return msg;
        }
    }

}
